/**Mathnetics Copyright (c) 2008 Shane Steinert-Threlkeld
Dual-licensed under the MIT and GNU GPL Licenses.
For more information, see LICENSE file */

/**The Vector class includes instance methods and static objects/methods.  It has no dependencies but is automatically included with Matrix.js.
@class mathnetics.Vector
*/

mathnetics.Vector = function() {
	/**the components of the Vector
	@variable {Number[]} components
	*/
	this.components = new Array();
	/**the dimension of the Vector (length of components array) 
	@variable {int} n
	*/
	this.n = this.components.length;
};

mathnetics.Vector.prototype = {

	/**Gets the component of the vector at index.
	@function {public Number} get
	@param {int} index - the index for which you want the component
	@return {Number} the (index)'th component of the vector */
	get: function(index) {
		return (index < 1 || index > this.components.length) ? null : this.components[index - 1];
	},

	/**Maps the vector to a new vector by the supplied function.
	@function {public Vector} map
	@param {Function} func - the mapping function
	@return {Vector} the new vector generated by the map */
	map: function(func) {
		var elements = new Array();
		if(typeof func == 'function') {
			this.toEach(function(x, i, k) {
				elements.push(func(x, i, k));
			});
			return mathnetics.Vector.create(elements);
		} else {
			return null;
		}
	},

	/**Performs a supplied function on every element of the vector. Used by map.
	@function {public void} toEach
	@param {Function} func the function to perform on each element*/
	toEach: function(func) {
		for(i = 0; i < this.n; i++) {
			if(typeof k == 'undefined') {
				func(this.components[i], i, 0);
			} else {
				func(this.components[i], i, k);
			}
		}
	},

	/**Tests to see if two vectors are equal to each other.
	@function {public boolean} equalTo
	@param {Vector} vec - the vector to compare to
	@return true iff vectors are equal (all components are equal and have same dimension), false otherwise*/
	equalTo: function(vec) {
		if(this.n != vec.n) {
			return false;
		}
		for(i = 0; i < this.n; i++) {
			if(Math.abs(this.components[i] - vec.components[i]) > mathnetics.zero) {
				return false;
			}
		}
		return true;
	},

	/**Normalizes the vector and returns the new unit vector.
	@function {public Vector} normalize
	@return a new vector that is the normalized version of the original vector */
	normalize: function() {
		var length = this.getLength();
		if(length == 0) {
			return mathnetics.Vector(this);
		}
		return this.map(function(x) {
				return x / length;
		});
	},

	/**Tests to see if 2 vectors are linearly independent from each other.
	@function {public boolean} linearlyIndependent
	@param {Vector} vec - the vector to test for independence 
	@return true iff vectors are independent, false otherwise (null if vectors not of same size) */
	linearlyIndependent: function(vec) {
		if(this.n != vec.n) {
			return null;
		}
		V = this.map(function(x, i) { return vec.components[i] / x });
		for(i = 1; i < this.n; i++) {
			if(V.get(i) != V.get(i+1)) {
				return true;
			}
		}
		return false;
	},

	/**Tests to see if 2 vectors are linearly dependent.
	@function {public boolean} linearlyDependent
	@param {Vector} vec - the vector to compare with
	@return true iff vectors are dependent, false otherwise */
	linearlyDependent: function(vec) {
		return !(this.linearlyIndependent(vec));
	},

	/**Adds a vector to the supplied vector.
	@function {public Vector} add
	@param {Vector} vec - the vector to be added.
	@return a new vector that is the sum of the original and supplied.*/
	add: function(vec) {
		if(vec.dimension()) {
			if(this.dimension() == vec.dimension()) {
				return this.map(function(x, i) { return x + vec.components[i]; });
			} else {
				return null;
			}
		} else {
			return null;
		}
	},

	/**Subtracts a vector to the supplied vector.
	@function {public Vector} subtract
	@param {Vector} vec - the vector to be subtracted.
	@return a new vector that is the result of the subtraction.*/
	subtract: function(vec) {
		if(vec.dimension()) {
			if(this.dimension() == vec.dimension()) {
				return this.map(function(x, i) { return x - vec.components[i]; });
			} else {
				return null;
			}
		} else {
			return null;
		}
	},

	/**Rounds all the elements of the vector and returns the copy.
	@function {public Vector} round
	@return a copy of the original vector, but rounded */
	round: function() {
		return this.map(function(x) {
			return Math.round(x);
		});
	},

	/**Multiplies a vector by a scalar k.
	@function {public Vector} multiplyBy
	@param {Number} k - the scalar by which to multiply the vector
	@return a new vector that is the scalar multiplication */
	multiplyBy: function(k) {
		return this.map(function(x) {
			return x*k;
		});
	},

	/**Returns the scalar dot product of this vector and the supplied vector.
	@function {public Number} dot
	@param {Vector} vec - the vector with which to compute the dot product
	@return null if vector is not a vector or is a different dimension, the dot product of the two vectors otherwise.
	*/
	dot: function(vec) {
		var dotProduct = 0;
		if(vec.dimension()) {
			if(this.n == vec.n) {
				for(i = 0; i < this.n; i++) {
					dotProduct += this.components[i]*vec.components[i];
				}
				return dotProduct;
			} else {
				return null;
			}
		} else {
			return null;
		}
	},

	/**Computes the cross product of two 3-dimensional vectors.
	@function {public Vector} cross
	@param {Vector} vec - the vector with which to take the cross product
	@return a new vector that is the cross product; null if either of the vectors is not 3-dimensional */
	cross: function(vec) {
		if(this.n == 3 && vec.n == 3) {
			var a = this.components;
			var b = vec.components;
			return mathnetics.Vector.create([
				a[1]*b[2] - a[2]*b[1],
				a[2]*b[0] - a[0]*b[2],
				a[0]*b[1] - a[1]*b[0]
			]);
		} else {
			return null;
		}
	},

	/**Projects one Vector onto another.  Gives the component of this Vector in the supplied Vector's direction.
	@function {public Vector} resolute
	@param {Vector} vec - (or mathnetics.point2D/3D object) the vector onto which to project this vector
	@return a new  Vector (or mathnetics.point2D/3D object), the original projected onto vec */
	resolute: function(vec) {
		if(vec.dimension()) {
			var direction = vec.normalize();
			return direction.multiplyBy(this.dot(direction));
		} else {
			return null;
		}
	},

	/**Reflects the Vector or mathnetics.point2D/3D in the given point, line, or plane
	@function {public Vector} reflectionIn
	@param {Object} obj - the Line, Plane, or Vector/mathnetics.point2D/3D in which to reflect this
	@return a new Vector or mathnetics.point3D (depends on which calls this function) that is reflected */
	reflectionIn: function(obj) {
		if(obj.base) { //obj is a Line or Plane
			var point = mathnetics.point3D.create(this.get(1),this.get(2),(this.get(3) || 0));
			var closest = obj.pointClosestTo(point);
			var vec = closest.add(closest.subtract(point));
			if(this instanceof mathnetics.point3D) {
				return new mathnetics.point3D(vec.get(1), vec.get(2), vec.get(3));
			} else if (this instanceof mathnetics.Vector) {
				return new Vector(vec);
			}
		} else { //handles mathnetics.point2D/3D or Vector
			if(this.n != obj.n) {
				return null;
			}
			return this.map(function(x, i) { return obj.get(i+1) + (obj.get(i+1) - x); });
		}
	},

	/**Returns a negative copy of the Vector (or point).
	@function {public Vector} negative
	@return a new, negative copy of the original Vector or mathnetics.point2D/3D */
	negative: function() {
		return this.map(function(x) { return -x; });
	},

	/**Converts a Vector into a 1-column matrix.
	@function {public Matrix} toColumnMatrix
	@return the column Matrix representation of the Vector */
	toColumnMatrix: function() {
		var els = new Array();
		for(i = 1; i <= this.n; i++) {
			els[i-1] = new Array();
			els[i-1][0] = this.get(i);
		}
		return mathnetics.Matrix.create(els);
	},

	/**Converts a Vector into a 1-row matrix.
	@function {public Matrix} toRowMatrix
	@return the row Matrix representation of the Vector */
	toRowMatrix: function() {
		var els = new Array();
		els[0] = this.components;
		return mathnetics.Matrix.create(els);
	},

	/**Returns the length of the vector by modulus definition: length = sqrt(this.dot(this))
	@function {public Number} getLength
	@return the length of the vector*/
	getLength: function() {
		return Math.sqrt(this.dot(this));
	},

	/**Finds and returns the angle between two vectors.
	@function {public Number} angleBetween
	@param {Vector} vec - the vector from which to find the angle
	@return the angle (in radians) between the two vectors */
	angleBetween: function(vec) {
		if(this.dimension() == vec.dimension()) {
			var len1 = this.getLength(), len2 = vec.getLength();
			if(len1 * len2 == 0) {
				return null;
			}
			var theta = this.dot(vec) / (len1*len2);
			if(theta < -1) {
				theta = -1;
			}
			if(theta > 1) {
				theta = 1;
			}
			return Math.acos(theta);
		} else {
			return null;
		}
	},

	/**Tests if two vectors are parallel.
	@function {public boolean} isParallel
	@param {Vector} vec - the vector to compare 
	@return true iff vectors are parallel, false or null otherwise */
	isParallel: function(vec) {
		var angle = this.angleBetween(vec);
		return (angle == null) ? null : (angle <= mathnetics.zero);
	},

	/**Tests if two vectors are antiparallel.
	@function {public boolean} isAntiParallel
	@param {Vector} vec - the vector to compare 
	@return true iff vectors are antiparallel, false or null otherwise */
	isAntiParallel: function(vec) {
		var angle = this.angleBetween(vec);
		return (angle == null) ? null : (Math.abs(angle - Math.PI) <= mathnetics.zero);
	},

	/**Tests if two vectors are perpendicular.
	@function {public boolean} isPerpendicular
	@param {Vector} vec - the vector to compare 
	@return true iff vectors are perpendicular, false or null otherwise */
	isPerpendicular: function(vec) {
		var dotProduct = this.dot(vec);
		return (dotProduct == null) ? null : (Math.abs(dotProduct) <= mathnetics.zero);
	},

	/**Returns the distance between the vector and an object, as if the vector were a point in its n-space.
	@function {public Number} distanceFrom
	@param {Object} obj - the object (Vector, Line, Plane, etc) from which to get the distance
	@return the distance between the vector and the object */
	distanceFrom: function(obj) {
		//throw in this if for when other objects (line, plane, etc) exist
		if(obj.dimension()) {
			if(this.dimension() == obj.dimension()) {
				var sum = 0, residual;
				this.toEach(function(x, i) {
					residual = x - obj.components[i];
					sum += Math.pow(residual,2);
				});
				return Math.sqrt(sum);
			} else {
				return null;
			}
		} else {
			return null;
		}
	},

	/**Sets and or returns the dimension of the vector: if no parameter supplied, returns dimension; if number given, sets dimension.
	@function {public int} dimension
	@param {optional int} n - the dimension to set the array.  If n &gt; current dimension, 0s added as all new components; if n &lt; current dimension, all components greater than that dimension are deleted.
	@return the dimension of the vector. */
	dimension: function(n) {
		if(n) {
			if(n > this.n) {
				for(i = this.components.length; i < n; i++) {
					this.components.push(0);
				}
			} else {
				this.components.splice((this.n - n) - 1, (this.n - n));
			}
			this.n = n;
		}
		return this.components.length;
	},

	/**Makes a copy of the given vector in 3 dimensions.
	@function {public Vector} make3D
	@return a new vector that is 3-D, with components decided according to the dimension() method (i.e. 0 if n&lt;3, removed if n &gt; 3)
	@see dimension */
	make3D: function() {
		var newVec = mathnetics.Vector.create(this);
		newVec.dimension(3);
		return newVec;
	},
	
	/**Sets all values within range of mathnetics.zero (set in mathnetics.js) to val.
	@function {public Vector} snapTo
	@param {Number} val - the number to snap the vector to 
	@return this Vector snapped to the given value */
	snapTo: function(val) {
		return this.map(function(x) {
			return (Math.abs(x - val) <= mathnetics.zero) ? val : x;
		});
	},

	/**Returns the first occurence of x in the vector.
	@function {public int} indexOf
	@param {Number} x - the element to search for
	@return the index of the first occurence of element x; null if never occurs */
	indexOf: function(x) {
		var index = null;
		for(i = 0; i < this.n; i++) {
			if(index == null && this.components[i] == x) {
				index = i + 1;
			}
		}
		return index;
	},

	/**Finds and returns the element with the largest absolute value.
	@function {public Number} max
	@return the absolute largest element */
	max: function() {
		var maximum = 0;
		for(i = 0; i < this.n; i++) {
			if(Math.abs(this.components[i]) > Math.abs(maximum)) {
				maximum = this.components[i];
			}
		}
		return maximum;
	},

	/**Method for setting and getting the elements of a vector.
	@function {public Number[]} elements
	@param {optional Number[]} els - if supplied, will set the vector's elements to els.
	@return the elements of the vector as an array */
	elements: function(els) {
		if(els) {
			this.components = els;
			this.n = this.components.length;
		}
		return this.components;
	},

	/**Adds a new element to the vector and updates the dimension.
	@function {public Vector} addElement
	@param {Number} el - the number to add to the vector
	@return this vector, updated
	 */
	addElement: function(el) {
		this.components.push(el);
		this.n++;
		return this;
	},

	/**Converts a Vector to a point object of appropriate dimension in homogeneous coordinates.  If called from a mathnetics.point2D or mathnetics.point3D object, acts as another way to clone the point.
	@function {public Object} toPoint
	@return either a mathnetics.point2D or mathnetics.point3D object, depending on size of Vector, with same  coordinates as the Vector's components */
	toPoint: function() {
		if(this.n == 2) {
			return mathnetics.point2D.create(this.get(1),this.get(2));
		} else if(this.n == 3 || this.n == 4) {
			return mathnetics.point3D.create(this.get(1),this.get(2),this.get(3));
		} else {
			return null;
		}
	},

	/**Resets the elements of the Vector; used by constructor.
	@function {public mathnetics.Vector} setElements
	@paramset fromArray
	@param {Number[]} elements - Array to become the components of the Vector
	@paramset clone
	@param {Vector} vec - the vector to clone
	@return this Vector, updated */
	setElements: function(elements) {
		if(elements instanceof Array) {
			this.components = elements;
		} 
		//allows easy cloning of a vector
		if(elements instanceof mathnetics.Vector) {
			//slice so that changes to new vector do not affect old one
			this.components = elements.components.slice();
		}
		this.n = this.components.length;
		return this;
	},

	/**Clones the current Vector into a new one.
	@function {public mathnetics.Vector} dup
	@return a new clone of current Vector */
	dup: function() {
		return mathnetics.Vector.create(this);
	},

	/**Returns a string representation of the vector.
	@function {public String} toString
	@return the string representation of the vector.*/
	toString: function() {
		return "[" + this.components.join(" ") + "]";
	}

}; //end Vector prototype

/**Constructor function.  Takes an Array or a Vector and creates a Vector object.
@constructor mathnetics.Vector.create 
@see mathnetics.Vector.setElements */
mathnetics.Vector.create = function(elements) {
	var V = new mathnetics.Vector();
	return V.setElements(elements);
};

mathnetics.extend(mathnetics.Vector, {


//The three standard unit vectors in 3 dimensions
/**The unit vector [1,0,0]
@variable {public static mathnetics.Vector} mathnetics.Vector.i */
i: mathnetics.Vector.create([1,0,0]),
/**The unit vector [0,1,0]
@variable {public static mathnetics.Vector} mathnetics.Vector.j */
j: mathnetics.Vector.create([0,1,0]),
/**The unit vector [0,0,1]
@variable {public static mathnetics.Vector} mathnetics.Vector.k */
k: mathnetics.Vector.create([0,0,1]),

/**Creates and returns a new vector with n random elements less than or equal to max.
@function {public static Vector} mathnetics.Vector.random
@param {int} n - the number of components the vector should be
@param {optional Number} max - the maximum value the elements can be; if not specified, values will be in rand [0,1) 
@return a new vector with random components */
random: function(n, max) {
	var elements = new Array();
	for(i = 0; i < n; i++) {
		if(max) {
			elements[i] = Math.floor(Math.random()*(max+1));
		} else {
			elements[i] = Math.random();
		}
	}
	return mathnetics.Vector.create(elements);
},

/**Creates and returns  a new vector of dimension n with all 0 components.
@function {public static Vector} mathnetics.Vector.zero
@param {int} n - the dimension of the new zero vector
@return a new zero vector of dimension n */
zero: function(n) {
	var elements = new Array();
	for(i = 0; i < n; i++) {
		elements[i] = 0;
	}
	return mathnetics.Vector.create(elements);
}

});
